Skip to content

Go 迭代器:把“怎样取下一个”留给集合 ​

一个目录里有十万个文件,调用者只想找到前三个符合条件的名字。如果 API 先返回一个大切片,它往往要先走完整个目录;如果 API 接收回调,调用者又得放弃熟悉的 for range。Go 的函数迭代器把这两件事接在了一起:提供者负责逐个产生值,使用者仍然写循环,并能用 break 叫停。range 遍历函数和标准库 iter 从 Go 1.23 开始可用。[1]

本文要讲清的是这个停止信号如何传回去,切片、map 怎样接上这个协议,以及读取文件一类有资源、有错误的过程该怎样收尾。示例在 Go 1.27.2、Linux/amd64 上运行;语言规则、标准库约定与本次输出会分别说明。

先认接口:迭代器究竟是什么类型 ​

普通的 range xs 由语言知道如何遍历切片。函数迭代器则把这个动作交给一个函数:循环开始时调用它,并给它一个叫 yield 的回调。每次调用 yield(v),就执行一次循环体;回调返回 false 时,提供者必须停止。yield 是惯用参数名,不是关键字。[1:1]

语言接受三种函数形状,区别只在每次交出几个值:

迭代函数的形状循环能接收的值常见用途
func(func() bool)不接值只通知发生了一次迭代
func(func(V) bool)一个值依次交出名字、记录或数字
func(func(K, V) bool)两个值下标与元素、键与值

iter 给后两种形状起了泛型类型名。把定义展开,协议就没有那么神秘了:[2]

go
type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)

它们不是包含游标字段的结构体,也没有强制的 Next 方法。迭代状态通常放在函数的局部变量或闭包里。类型名里的 2 只表示交出两个值,第二个值不一定是 map 的 value,也可以是下面会讲到的 error。

沿着一次 break,看清控制权怎样来回走 ​

下面这个迭代器把动作打印出来。返回的函数真正开始运行时才打印 start,而不是调用 Count(4) 时就开始枚举。

go
func Count(n int) iter.Seq[int] {
    return func(yield func(int) bool) {
        fmt.Println("start")
        defer fmt.Println("cleanup")
        for i := 0; i < n; i++ {
            fmt.Println("offer", i)
            if !yield(i) {
                fmt.Println("stop")
                return
            }
            fmt.Println("resume", i)
        }
    }
}

seq := Count(4)
fmt.Println("created")
for n := range seq {
    fmt.Println("body", n)
    if n == 1 {
        break
    }
}
fmt.Println("loop ended")

实际输出:

text
created
start
offer 0
body 0
resume 0
offer 1
body 1
stop
cleanup
loop ended

读这段输出,最有用的是盯住 body 1 后面的变化:循环体要求退出,yield(1) 返回 false,迭代器随即 return,它自己的 defer 执行,外层循环才结束。没有生成 2 和 3,也没有先把四个数字存进切片。[1:2][2:1]

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
sequenceDiagram
    participant R as for range
    participant I as 迭代函数
    participant B as 循环体
    R->>I: 传入 yield,开始遍历
    I->>B: yield(0)
    B-->>I: true,继续
    I->>B: yield(1)
    B-->>I: false,循环体 break
    I->>I: return,执行 defer
    I-->>R: 迭代结束
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
sequenceDiagram
    participant R as for range
    participant I as 迭代函数
    participant B as 循环体
    R->>I: 传入 yield,开始遍历
    I->>B: yield(0)
    B-->>I: true,继续
    I->>B: yield(1)
    B-->>I: false,循环体 break
    I->>I: return,执行 defer
    I-->>R: 迭代结束

这也解释了最重要的一条实现规则:每个可能继续生产数据的 yield 调用,都要处理它返回的布尔值。 忽略 false 后继续调用,就是违反协议。下面的反例已单独运行:

go
func bad(yield func(int) bool) {
    yield(1)
    yield(2) // 调用者已经 break,仍继续交出数据
}

for range bad {
    break
}

Go 1.27.2 检测到了它,panic 的内容为:

text
runtime error: range function continued iteration after function for loop body returned false

错误文字属于这次实现的输出,不应拿来做跨版本业务判断。应该遵守的是语言协议:yield 返回 false 后不能再调用;迭代函数返回后也不能留着它供以后调用。不要把这个回调存到后台 goroutine 里,等循环结束后继续推送。[1:3]

惰性来自代码怎样写,不来自 Seq 这个名字 ​

上面的 Count 在被遍历时才计算下一个数字,所以具有惰性。iter.Seq 本身只规定函数签名;如果函数先读完全部输入,再挨个 yield,它照样会提前消耗内存和时间。讨论“迭代器节省了什么”时,要看生产者是否真正按需工作。[2:2]

筛选是一个适合逐项完成的动作。下面的 Filter 接收一个迭代器,再返回一个迭代器:

go
func Filter[T any](src iter.Seq[T], keep func(T) bool) iter.Seq[T] {
    return func(yield func(T) bool) {
        for v := range src {
            if keep(v) && !yield(v) {
                return
            }
        }
    }
}

src := slices.Values([]int{1, 2, 3, 4})
even := Filter(src, func(n int) bool { return n%2 == 0 })
fmt.Println(slices.Collect(even))

实际输出:

text
[2 4]

这里有两层停止传播。下游不想要更多值时,Filter 的 yield 返回 false;Filter 随即退出自己的 for range src,于是上游也收到 false。文末测试用一个计数器验证:输入为 0 到 9,保留偶数,拿到 2 就 break,上游只访问了 0、1、2 三个值,而且其清理函数已经执行。

筛选可以流式进行,排序则通常必须先收集全部数据。slices.Sorted(seq) 返回排好序的切片,调用完成前就得知道全部元素。不要把这些函数连在一起后,就认为整条链都不再分配或都支持无限输入。[3]

slices 与 maps:用现成入口接上自己的数据 ​

先看返回类型,再看名字,会更容易选对函数。下面这些迭代相关入口从 Go 1.23 起进入标准库;slices 和 maps 包本身更早,在 Go 1.21 加入。[3:1][4][5]

数据方向常用入口得到什么要留意什么
切片 → 值slices.Values(s)iter.Seq[E]只给元素,不给下标
切片 → 下标、值slices.All(s)iter.Seq2[int,E]下标从小到大
切片 → 倒序下标、值slices.Backward(s)iter.Seq2[int,E]仍是原切片下标
迭代器 → 切片slices.Collect(seq)新切片消费整个迭代器
迭代器 → 已有切片slices.AppendSeq(s,seq)追加后的切片要接住返回值
迭代器 → 有序切片slices.Sorted(seq)排序后的新切片需要消费所有输入
map → 键、值、键值对maps.Keys、Values、AllSeq 或 Seq2不承诺遍历顺序
键值对迭代器 → mapmaps.Collect(seq)新 map同一键后面的值覆盖前面的值
键值对迭代器 → 已有 mapmaps.Insert(m,seq)修改传入的 map目标必须能够接收写入

Seq2 的使用与遍历切片很接近:

go
for i, name := range slices.All([]string{"Ada", "Lin"}) {
    fmt.Println("pair", i, name)
}

m := map[string]int{"b": 2, "a": 1}
fmt.Println("keys", slices.Sorted(maps.Keys(m)))
copied := maps.Collect(maps.All(m))
fmt.Println("map equal", maps.Equal(m, copied))

实际输出:

text
pair 0 Ada
pair 1 Lin
keys [a b]
map equal true

maps.Keys 返回的不是 []string,因此不能直接对它做下标访问。需要切片就 Collect;需要稳定顺序就 Sorted。连续运行几次偶然拿到相同顺序,也不能证明 map 有序。无序是接口约定,不需要等到实验恰好出现顺序变化才成立。[4:1]

迭代器通常没有替你做快照 ​

go
xs := []int{10, 20}
live := slices.Values(xs)
xs[0] = 99
fmt.Println("live", slices.Collect(live))

实际输出:

text
live [99 20]

slices.Values 保留着对那段切片数据的访问,创建它时并没有复制元素。若需要创建时的快照,应先明确复制,例如 slices.Values(slices.Clone(xs))。而 Clone 只做元素赋值;元素里还有切片、map、指针时,更深层的数据仍可能共享。[3:2]

同样,maps.All 不替 map 加锁。并发读写的同步责任仍在调用者,不能因为普通 range 换成函数迭代器,就去掉原来的锁。本文的 -race 验证只检查了附录中运行的路径,不是对所有迭代器实现的安全保证。[4:2][6]

Go 1.24 还加入了 strings.SplitSeq、FieldsSeq 等单次使用迭代器,bytes 也有对应入口。它们适合只想依次消费分段的场景;如果后面要随机访问或反复使用结果,收集成切片反而可能更清楚。这里的选择是 API 使用建议,不能代替针对真实输入的性能测试。[7]

读文件这样的过程,错误和资源该放在哪里 ​

iter.Seq 没有内置错误通道。一个实用约定是返回 iter.Seq2[T,error]:正常项的错误为 nil;读取失败时交出错误,然后结束。这只是本例的 API 设计,不是语言替所有迭代器规定的格式。

go
// Lines 消费调用者给的 Reader,不负责回退或关闭它。
func Lines(r io.Reader) iter.Seq2[string, error] {
    return func(yield func(string, error) bool) {
        sc := bufio.NewScanner(r)
        for sc.Scan() {
            if !yield(sc.Text(), nil) {
                return
            }
        }
        if err := sc.Err(); err != nil {
            yield("", err)
        }
    }
}

lines := Lines(strings.NewReader("first\nsecond\n"))
for line, err := range lines {
    fmt.Println("line", line, err)
}
again := 0
for range lines {
    again++
}
fmt.Println("second traversal", again)

实际输出:

text
line first <nil>
line second <nil>
second traversal 0

第二次没有内容,因为两次遍历消费的是同一个 Reader,第一遍已经读到结尾。迭代器能否重放必须由提供者说明,不能只看返回类型。iter 包文档专门区分可重复使用与单次使用的迭代器。[2:3]

这个实现还有三个边界:

  • Scanner 有 token 大小限制。正常结束和扫描失败都会让 Scan 返回 false,必须检查 Err。文末测试确实用 70,000 字节的一行触发了错误。需要长行时应设计可配置上限,或改用 bufio.Reader。[8]
  • 调用者提前 break 后,没再读取的内容是否损坏还不知道。不能声称“没有收到错误,就证明整个文件正常”。这由停止读取的行为直接得出。
  • 这里 Reader 由调用者提供,因此也由调用者决定何时关闭。若换成 FileLines(path),由迭代器内部打开文件,打开动作和 defer Close 应放在返回的函数里:未遍历就不打开,退出时才有明确的关闭位置。关闭错误是否需要反馈,也要在 API 设计中约定。Seq 不自动承担这些工作。[2:4][9]

需要手动拿“下一项”时,再用 iter.Pull ​

合并两路有序输入时,调用者往往需要交替拿两边的下一项。此时 for range 不容易直接表达,iter.Pull 可以把推送式的 Seq[V] 转成两个函数:next() (V,bool) 与 stop()。双值版本叫 Pull2。[10]

go
next, stop := iter.Pull(Count(3))
defer stop()
v, ok := next()
fmt.Println("next", v, ok)
stop()
v, ok = next()
fmt.Println("after stop", v, ok)

与前面的 Count 搭配,实际输出为:

text
start
offer 0
next 0 true
stop
cleanup
after stop 0 false

第一次 next 返回后,生产者停在第一次交出值的位置,它的 defer 还没有执行。调用 stop 后,生产者才能结束并清理。这就是为什么创建后应立即 defer stop():只消费一部分时,也必须给它结束机会。遍历已经耗尽或已经停止后,再调用 stop 是安全的;之后的 next 返回零值和 false。[10:1]

next 与 stop 不应该被多个 goroutine 同时调用。Pull 解决的是调用方式的转换,不是并发队列或线程安全包装。也没有必要为了统一写法,把每个普通 for range 都先转换一遍。[10:2]

取消需要显式检查,break 不会打断任意阻塞 ​

前面的停止都是合作式的:生产者正在调用 yield,因此可以立即收到 false。若生产者卡在网络读取、数据库查询或自己写的无限计算里,外层 break 不能越过这段代码去中断它。要让任务响应取消,阻塞操作本身也要支持 context、截止时间或其他退出机制。[1:4][11]

对于纯计算,最小写法是在生成下一项之前检查 context:

go
func UntilCanceled(ctx context.Context, n int) iter.Seq[int] {
    return func(yield func(int) bool) {
        for i := 0; i < n; i++ {
            if ctx.Err() != nil {
                return
            }
            if !yield(i) {
                return
            }
        }
    }
}

ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for n := range UntilCanceled(ctx, 100) {
    fmt.Println("cancel demo", n)
    cancel()
}

实际只输出:

text
cancel demo 0

本例用“静默结束”表示取消。如果调用者必须区分“正常读完”和“被取消”,应把错误纳入协议,例如返回 Seq2[T,error] 并交出 ctx.Err()。取消语义、重复使用、数据所有权这三件事,比给类型取个 Iterator 名字更影响调用者能否正确使用它。

版本检查与本次验证 ​

安装了新工具链,不等于每个包都自动采用最新语言版本。附录的反例模块写着 go 1.22,在 Go 1.27.2 下仍无法编译函数 range:[12]

text
cannot range over func(yield func(int) bool) {…} ...:
requires go1.23 or later (-lang was set to go1.22; check go.mod)

因此给旧项目引入迭代器时,要一起检查工具链、go.mod 的语言版本和使用的具体 API 的加入版本。不要只把 CI 的 Go 二进制换新。

2026-10-13 的实际验证包括:主程序输出、四个测试、go test -race、go vet、go 1.22 编译失败反例、违反 yield 协议的运行反例。正向测试全部通过;两个反例均出现预期结果。没有测量迭代器与切片的吞吐、分配或真实文件系统负载,所以本文不提供固定性能倍数。

参考资料 ​

附录:全部复现文件与命令 ​

保存为下面的目录。所有依赖都来自标准库,不需要下载模块。testdata 里的反例不会被普通的 go test ./... 当作包遍历,须按命令单独执行。

text
iteratorlab/
├── go.mod
├── main.go
├── main_test.go
└── testdata/
    ├── oldversion/{go.mod,main.go}
    └── badprotocol/{go.mod,main.go}

运行命令:

bash
go version                 # 本次为 go1.27.2 linux/amd64
go run .
go test -count=1 -v ./...
go test -count=1 -race ./...
go vet ./...
(cd testdata/oldversion && go build .)  # 预期退出码 1
(cd testdata/badprotocol && go run .)  # recover 打印协议 panic

go.mod ​

go.mod
module example.com/iteratorlab

go 1.27

main.go ​

代码块 · 115 行 · 点击收起 / 展开
go
package main

import (
	"bufio"
	"context"
	"fmt"
	"io"
	"iter"
	"maps"
	"slices"
	"strings"
)

func Count(n int) iter.Seq[int] {
	return func(yield func(int) bool) {
		fmt.Println("start")
		defer fmt.Println("cleanup")
		for i := 0; i < n; i++ {
			fmt.Println("offer", i)
			if !yield(i) {
				fmt.Println("stop")
				return
			}
			fmt.Println("resume", i)
		}
	}
}

func Filter[T any](src iter.Seq[T], keep func(T) bool) iter.Seq[T] {
	return func(yield func(T) bool) {
		for v := range src {
			if keep(v) && !yield(v) {
				return
			}
		}
	}
}

// Lines consumes r; it neither rewinds nor closes the caller's reader.
func Lines(r io.Reader) iter.Seq2[string, error] {
	return func(yield func(string, error) bool) {
		sc := bufio.NewScanner(r)
		for sc.Scan() {
			if !yield(sc.Text(), nil) {
				return
			}
		}
		if err := sc.Err(); err != nil {
			yield("", err)
		}
	}
}

func UntilCanceled(ctx context.Context, n int) iter.Seq[int] {
	return func(yield func(int) bool) {
		for i := 0; i < n; i++ {
			if ctx.Err() != nil {
				return
			}
			if !yield(i) {
				return
			}
		}
	}
}

func main() {
	seq := Count(4)
	fmt.Println("created")
	for n := range seq {
		fmt.Println("body", n)
		if n == 1 {
			break
		}
	}
	fmt.Println("loop ended")

	fmt.Println("even", slices.Collect(Filter(slices.Values([]int{1, 2, 3, 4}), func(n int) bool { return n%2 == 0 })))
	for i, name := range slices.All([]string{"Ada", "Lin"}) {
		fmt.Println("pair", i, name)
	}
	m := map[string]int{"b": 2, "a": 1}
	fmt.Println("keys", slices.Sorted(maps.Keys(m)))
	copied := maps.Collect(maps.All(m))
	fmt.Println("map equal", maps.Equal(m, copied))
	xs := []int{10, 20}
	live := slices.Values(xs)
	xs[0] = 99
	fmt.Println("live", slices.Collect(live))

	lines := Lines(strings.NewReader("first\nsecond\n"))
	for line, err := range lines {
		fmt.Println("line", line, err)
	}
	again := 0
	for range lines {
		again++
	}
	fmt.Println("second traversal", again)

	next, stop := iter.Pull(Count(3))
	defer stop()
	v, ok := next()
	fmt.Println("next", v, ok)
	stop()
	v, ok = next()
	fmt.Println("after stop", v, ok)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	for n := range UntilCanceled(ctx, 100) {
		fmt.Println("cancel demo", n)
		cancel()
	}
}

main_test.go ​

代码块 · 77 行 · 点击收起 / 展开
go
package main

import (
	"context"
	"iter"
	"slices"
	"strings"
	"testing"
)

func TestFilterStopsUpstream(t *testing.T) {
	visited, closed := 0, false
	src := iter.Seq[int](func(yield func(int) bool) {
		defer func() { closed = true }()
		for i := 0; i < 10; i++ {
			visited++
			if !yield(i) {
				return
			}
		}
	})
	got := []int{}
	for n := range Filter(src, func(n int) bool { return n%2 == 0 }) {
		got = append(got, n)
		if n == 2 {
			break
		}
	}
	if !slices.Equal(got, []int{0, 2}) || visited != 3 || !closed {
		t.Fatalf("got=%v visited=%d closed=%v", got, visited, closed)
	}
}

func TestPullCleanup(t *testing.T) {
	closed := false
	seq := iter.Seq[int](func(yield func(int) bool) {
		defer func() { closed = true }()
		yield(42)
	})
	next, stop := iter.Pull(seq)
	defer stop()
	if v, ok := next(); v != 42 || !ok {
		t.Fatal(v, ok)
	}
	if closed {
		t.Fatal("iterator already returned")
	}
	stop()
	stop()
	if !closed {
		t.Fatal("missing cleanup")
	}
	if v, ok := next(); v != 0 || ok {
		t.Fatal(v, ok)
	}
}

func TestLinesError(t *testing.T) {
	count := 0
	for _, err := range Lines(strings.NewReader(strings.Repeat("x", 70_000))) {
		count++
		if err == nil {
			t.Fatal("expected scanner error")
		}
	}
	if count != 1 {
		t.Fatal(count)
	}
}

func TestCancel(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	if got := slices.Collect(UntilCanceled(ctx, 10)); len(got) != 0 {
		t.Fatal(got)
	}
}

testdata/oldversion/go.mod ​

go.mod
module example.com/olditerator

go 1.22

testdata/oldversion/main.go ​

go
package main

func main() {
	for range func(yield func(int) bool) { yield(1) } {
	}
}

testdata/badprotocol/go.mod ​

go.mod
module example.com/baditerator

go 1.27

testdata/badprotocol/main.go ​

go
package main

import "fmt"

func bad(yield func(int) bool) { yield(1); yield(2) }
func main() {
	defer func() { fmt.Println("panic:", recover()) }()
	for range bad {
		break
	}
}

  1. Go 规范:For statements,包括函数 range 的三种签名、yield 与提前退出规则;Go 1.23 语言变化。 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  2. Go 1.27.2 iter 包,包括 Seq、Seq2、单次使用、标准库约定;本次也以同版本 go doc iter 核对。 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  3. Go 1.27.2 slices 包,Values、All、Backward、Collect、AppendSeq、Sorted、Clone;固定源码 iter.go。 ↩︎ ↩︎ ↩︎

  4. Go 1.27.2 maps 包,All、Keys、Values、Insert、Collect;固定源码 iter.go。 ↩︎ ↩︎ ↩︎

  5. Go 1.21:新标准库包;Go 1.23:迭代器相关函数。 ↩︎

  6. Go 官方 FAQ:map 的并发访问;Go 竞态检测器。 ↩︎

  7. Go 1.27.2 strings.SplitSeq、FieldsSeq;Go 1.24 发布说明。 ↩︎

  8. Go 1.27.2 bufio.Scanner、Err、Buffer、MaxScanTokenSize。 ↩︎

  9. Go 1.27.2 os.Open、File.Close。 ↩︎

  10. Go 1.27.2 iter.Pull、Pull2。 ↩︎ ↩︎ ↩︎

  11. Go 1.27.2 context 包:Done 与 Err 的契约,取消信号需要操作自行响应。 ↩︎

  12. Go Modules Reference:go directive;Go 工具链:语言版本选择。 ↩︎

保留各书来源与许可 · 示例运行条件以正文为准